/**
 * Copyright (C) 2025 pony working team
 * SPDX-License-Identifier: MulanPSL-2.0
 */

/**
 * WARNING: If you build fatfs in Linux, the error is as follows:
 *  /usr/include/dirent.h:127:28: error: conflicting types for ‘DIR’
 *      127 | typedef struct __dirstream DIR;
 *
 * WORKAROUND: use posix/dirent.h instead of dirent.h
 *
 * NOTES: fatfs and mockfs are not compatible with each other on Linux.
 *        fatfs is a real file system, while mockfs is a mock file system.
 */

/* includes (local) ----------------------------------------------------------*/
#include "vfs_fatfs.h"
/* includes (standard library, system) ---------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* includes (other library) --------------------------------------------------*/
/* includes (project) --------------------------------------------------------*/
#include <vfs.h>

/* defines -------------------------------------------------------------------*/
/* typedefs ------------------------------------------------------------------*/
/* macro ---------------------------------------------------------------------*/
/* variables (extern) --------------------------------------------------------*/
/* variables (local) ---------------------------------------------------------*/
/* variables (global) --------------------------------------------------------*/

struct disk_man disk;

/* inlines -------------------------------------------------------------------*/
/* functions -----------------------------------------------------------------*/

/**
  * @brief  Links a compatible diskio driver/lun id and increments the number of active linked
  drivers.
  *
  * @note   The number of linked drivers (volumes) is up to 10 due to FatFs limits.
  * @param  ops [IN] pointer to the disk IO Driver structure
  * @param  priv [IN] private data
  * @param  lun [IN] only used for USB Key Disk to add multi-lun management
            else the parameter must be equal to 0

  * @retval Returns -1 in case of failure, otherwise return the drive (0 to volumes).
  */
static int fatfs_link_driver(const struct disk_ops *ops, uintptr_t priv, uint8_t lun)
{
    int ret = -1;
    int i;

    if (disk.num >= FF_VOLUMES) {
        return ret;
    }
    for (i = 0; i < FF_VOLUMES; i++) {
        if (disk.dev[i].ops != NULL) {
            continue;
        }
        disk.dev[i].state = STA_NOINIT;
        disk.dev[i].ops = ops;
        disk.dev[i].lun = lun;
        disk.dev[i].priv = priv;
        disk.num++;
        return i;
    }
    return ret;
}

/**
 * @brief  Unlinks a diskio driver and decrements the number of active linked drivers.
 *
 * @param  drive [IN] the disk drive (0 to volumes)
 * @param  lun   [IN] not used
 *
 * @retval Returns -1 in case of failure, otherwise return the drive (0 to volumes).
 */
static int fatfs_unlink_driver(uint8_t drive, uint8_t lun)
{
    int ret = -1;

    if (disk.num >= 1 && drive < FF_VOLUMES) {
        if (disk.dev[drive].ops != NULL) {
            disk.dev[drive].state = STA_NOINIT;
            disk.dev[drive].ops = 0;
            disk.dev[drive].lun = 0;
            disk.dev[drive].priv = 0;
            disk.num--;
            return drive;
        }
    }

    return ret;
}

static int fatfs_register(const struct disk_ops *ops, uintptr_t priv)
{
    return fatfs_link_driver(ops, priv, 0);
}

static int fatfs_unregister(uint8_t drive)
{
    return fatfs_unlink_driver(drive, 0);
}

static int fatfs_flags_get(int oflags)
{
    int flags = 0;

    switch (oflags & O_ACCMODE) {
    case O_RDONLY:
        flags |= FA_READ;
        break;
    case O_WRONLY:
        flags |= FA_WRITE;
        break;
    case O_RDWR:
        flags |= FA_READ | FA_WRITE;
        break;
    default:
        break;
    }

    if (oflags & O_CREAT) {
        flags |= FA_OPEN_ALWAYS;
    }

    if ((oflags & O_CREAT) && (oflags & O_EXCL)) {
        flags |= FA_CREATE_NEW;
    }

    if (oflags & O_TRUNC) {
        flags |= FA_CREATE_ALWAYS;
    }

    if (oflags & O_APPEND) {
        flags |= FA_READ | FA_WRITE | FA_OPEN_APPEND;
    }

    return flags;
}

static int fatfs_op_open(struct file *fp, const char *path, int flags, int mode)
{
    FRESULT res;
    FIL *f;
    FILINFO info = {0};

    f = (FIL *)malloc(sizeof(FIL));
    if (f == NULL) {
        return -1;
    }

    if (!(flags & O_CREAT) && (flags & O_TRUNC)) {
        res = f_stat(path, &info);
        if (res != FR_OK) {
            free(f);
            return -1;
        }
    }

    res = f_open(f, path, fatfs_flags_get(flags));
    if (res != FR_OK) {
        free(f);
        return -1;
    }
    fp->fl_data = (uintptr_t)f;

    return 0;
}

static int fatfs_op_close(struct file *fp)
{
    FRESULT res;
    FIL *f = (FIL *)fp->fl_data;

    if (f == NULL) {
        return -1;
    }

    res = f_close(f);
    if (res != FR_OK) {
        return -1;
    }
    free(f);

    return 0;
}

static int fatfs_op_read(struct file *fp, char *buf, size_t nbyte)
{
    UINT size = -1;
    FRESULT res;
    FIL *f = (FIL *)fp->fl_data;

    if (f == NULL) {
        return -1;
    }

    res = f_read(f, buf, nbyte, (UINT *)&size);
    if (res != FR_OK) {
        return -1;
    }
    return (int)size;
}

static int fatfs_op_write(struct file *fp, const char *buf, size_t nbyte)
{
    int size = -1;
    FRESULT res;
    FIL *f = (FIL *)fp->fl_data;

    if (f == NULL) {
        return -1;
    }

    res = f_write(f, buf, nbyte, (UINT *)&size);
    if (res != FR_OK || size == 0) {
        return -1;
    }
    return size;
}

static int fatfs_op_lseek(struct file *fp, int offset, int whence)
{
    FRESULT res;
    FIL *f = (FIL *)fp->fl_data;

    if (f == NULL) {
        return -1;
    }

    switch (whence) {
    case 0:   // SEEK_SET
        break;
    case 1:   // SEEK_CUR
        offset += f_tell(f);
        break;
    case 2:   // SEEK_END
        offset += f_size(f);
        break;
    default:
        return -1;
    }
    if (offset < 0) {
        return -1;
    }

    res = f_lseek(f, offset);
    if (res != FR_OK) {
        return -1;
    }
    return offset;
}

static int fatfs_op_sync(struct file *fp)
{
    FRESULT res;
    FIL *f = (FIL *)fp->fl_data;

    if (f == NULL) {
        return -1;
    }

    res = f_sync(f);
    if (res != FR_OK) {
        return -1;
    }
    return 0;
}

static int fatfs_op_stat(struct mount_point *mp, const char *path, struct stat *st)
{
    FRESULT res;
    FILINFO info = {0};

    res = f_stat(path, &info);
    if (res != FR_OK) {
        return -1;
    }

    st->st_size = info.fsize;
    if (info.fattrib & AM_DIR) {
        st->st_mode = S_IFDIR;
    } else {
        st->st_mode = S_IFREG;
    }

    return 0;
}

static int fatfs_op_unlink(struct mount_point *mp, const char *path)
{
    FRESULT res = f_unlink(path);
    if (res != FR_OK) {
        return -1;
    }
    return 0;
}

static int fatfs_op_rename(struct mount_point *mp, const char *from, const char *to)
{
    FRESULT res = f_rename(from, to);
    if (res != FR_OK) {
        return -1;
    }
    return 0;
}

static int fatfs_op_mkdir(struct mount_point *mp, const char *path, int mode)
{
    FRESULT res = f_mkdir(path);
    if (res != FR_OK) {
        return -1;
    }
    return 0;
}

static int fatfs_op_rmdir(struct mount_point *mp, const char *path)
{
    FRESULT res = f_rmdir(path);
    if (res != FR_OK) {
        return -1;
    }
    return 0;
}

static int fatfs_op_opendir(struct mount_point *mp, struct dir *dp, const char *path)
{
    FRESULT res;
    DIR *d;

    d = (DIR *)malloc(sizeof(DIR));
    if (dp == NULL) {
        return -1;
    }

    res = f_opendir(d, path);
    if (res != FR_OK) {
        free(d);
        return -1;
    }
    dp->dr_data = (uintptr_t)d;

    return 0;
}

static int fatfs_op_readdir(struct mount_point *mp, struct dir *dp, struct dirent *de)
{
    FRESULT res;
    DIR *d = (DIR *)dp->dr_data;
    FILINFO info;
    int len;

    if (d == NULL) {
        return -1;
    }

    res = f_readdir(d, &info);
    if (res != FR_OK) {
        return -1;
    }

    len = min(sizeof(info.fname), sizeof(de->d_name)) - 1;
    strncpy((char *)de->d_name, (const char *)info.fname, len);
    de->d_name[len] = '\0';
    de->d_size = info.fsize;

    if (info.fattrib == AM_DIR) {
        de->d_type = DT_DIR;
    } else {
        de->d_type = DT_REG;
    }

    return 0;
}

static int fatfs_op_closedir(struct mount_point *mp, struct dir *dp)
{
    FRESULT res;
    DIR *d = (DIR *)dp->dr_data;

    if (d == NULL) {
        return -1;
    }

    res = f_closedir(d);
    if (res != FR_OK) {
        return -1;
    }
    free(d);

    return 0;
}

static int fatfs_op_mount(struct mount_point *mp, va_list valist)
{
    int idx;
    FRESULT res;
    int ret = -1;
    char dpath[10] = {0};
    BYTE *work_buff = NULL;
    FATFS *fs = NULL;
    struct disk_ops *ops = va_arg(valist, struct disk_ops *);
    uintptr_t priv = va_arg(valist, uintptr_t);
    uint8_t *drive = va_arg(valist, uint8_t *);

    if (ops == NULL || drive == NULL) {
        return -1;
    }

    idx = fatfs_register(ops, priv);
    if (idx < 0) {
        return -1;
    }
    fs = (FATFS *)malloc(sizeof(FATFS));
    if (fs == NULL) {
        goto EXIT_UNREGISTER_DRV;
    }
    memset(fs, 0, sizeof(FATFS));
    sprintf(dpath, "%d:/", idx);
    res = f_mount(fs, (const TCHAR *)dpath, 1);
    if (res == FR_NO_FILESYSTEM) {
        work_buff = (BYTE *)malloc(FF_MAX_SS);
        if (work_buff == NULL) {
            goto EXIT_FREE_FS;
        }
        memset(work_buff, 0, FF_MAX_SS);
        MKFS_PARM parm = {FM_ANY, 2, 0, 0, 0};
        res = f_mkfs((const TCHAR *)dpath, &parm, work_buff, FF_MAX_SS);
        if (res == FR_OK) {
            res = f_mount(NULL, (const TCHAR *)dpath, 1);
            res = f_mount(fs, (const TCHAR *)dpath, 1);
        }
        free(work_buff);
    }
    if (res != FR_OK) {
        ret = -res;
        goto EXIT_FREE_FS;
    }
    *drive = idx;
    mp->mp_data = (uintptr_t)fs;

    return 0;

EXIT_FREE_FS:
    if (fs != NULL) {
        free(fs);
    }
EXIT_UNREGISTER_DRV:
    fatfs_unregister(idx);
    return ret;
}

static int fatfs_op_unmount(struct mount_point *mp, va_list valist)
{
    FATFS *fs = (FATFS *)mp->mp_data;
    char dpath[10] = {0};
    uint8_t drive = (uint8_t)va_arg(valist, int);

    sprintf(dpath, "%d:/", drive);
    fatfs_unregister(drive);
    f_mount(NULL, (const TCHAR *)dpath, 1);
    if (fs != NULL) {
        free(fs);
    }
    return 0;
}

static const struct f_ops fatfs_fops = {
    .open = fatfs_op_open,
    .close = fatfs_op_close,
    .read = fatfs_op_read,
    .write = fatfs_op_write,
    .lseek = fatfs_op_lseek,
    .sync = fatfs_op_sync,
};

static const struct m_ops fatfs_mops = {
    .stat = fatfs_op_stat,
    .unlink = fatfs_op_unlink,
    .rename = fatfs_op_rename,
    .mkdir = fatfs_op_mkdir,
    .rmdir = fatfs_op_rmdir,
    .opendir = fatfs_op_opendir,
    .readdir = fatfs_op_readdir,
    .closedir = fatfs_op_closedir,
    .mount = fatfs_op_mount,
    .unmount = fatfs_op_unmount,
};

static struct file_system fatfs_fs = {
    "fatfs",
    &fatfs_fops,
    &fatfs_mops,
};

int fatfs_init(void)
{
    if (vfs_fs_register(&fatfs_fs) != 0) {
        return -1;
    }

    return 0;
}

int fatfs_exit(void)
{
    if (disk.num > 0) {
        return -1;
    }
    if (vfs_fs_unregister(&fatfs_fs) != 0) {
        return -1;
    }
    return 0;
}

int fatfs_mount(const char *path, struct disk_ops *ops, uintptr_t priv, uint8_t *drive)
{
    return vfs_mount("fatfs", path, ops, priv, drive);
}

int fatfs_unmount(const char *path, uint8_t drive)
{
    return vfs_unmount(path, drive);
}
